]> dgit.raspbian.org Git - gnome-shell.git/commitdiff
brightnessManager: Handle monitor backlight brightness
authorSebastian Wick <sebastian.wick@redhat.com>
Sat, 21 Jun 2025 22:16:56 +0000 (00:16 +0200)
committerSebastian Wick <sebastian.wick@redhat.com>
Sun, 3 Aug 2025 01:34:28 +0000 (03:34 +0200)
This implements a backlight manager which creates a global and multiple
per monitor BrightnessScales and updates the backlight based on the
BrightnessScales changing and the backlight changing. It shows the OSD
in response to changes.

In the future it will also handle dimming, auto brightness and keyboard
shortcuts.

The idea is that the per monitor scale changes the ratio between monitor
brighnesses and the global scale scales the monitor brightnesses
accordingly.

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/3751>

js/js-resources.gresource.xml
js/misc/brightnessManager.js [new file with mode: 0644]
js/ui/main.js
po/POTFILES.in

index c116810a73eabee62e0788d30b86ea6fc151cfa4..dc182cc748cf6aa08d0dd72620921d43e703aa85 100644 (file)
@@ -16,6 +16,7 @@
 
     <file>misc/animationUtils.js</file>
     <file>misc/breakManager.js</file>
+    <file>misc/brightnessManager.js</file>
     <file>misc/config.js</file>
     <file>misc/dateUtils.js</file>
     <file>misc/dbusErrors.js</file>
diff --git a/js/misc/brightnessManager.js b/js/misc/brightnessManager.js
new file mode 100644 (file)
index 0000000..2e7cc5b
--- /dev/null
@@ -0,0 +1,253 @@
+import Gio from 'gi://Gio';
+import GObject from 'gi://GObject';
+
+import * as Main from '../ui/main.js';
+
+const SCALE_VALUE_CHANGE_EPSILON = 0.001;
+
+export const BrightnessManager = GObject.registerClass({
+    Signals: {
+        'changed': {},
+    },
+}, class BrightnessManager extends GObject.Object {
+    constructor() {
+        super();
+
+        this._globalScale = null;
+        this._monitorScales = new Map();
+
+        const monitorManager = global.backend.get_monitor_manager();
+        monitorManager.connectObject('monitors-changed',
+            this._monitorsChanged.bind(this), this);
+        this._monitorsChanged();
+    }
+
+    get globalScale() {
+        return this._globalScale;
+    }
+
+    get scales() {
+        return [...this._monitorScales.values()];
+    }
+
+    _monitorsChanged() {
+        const monitors = global.backend
+            .get_monitor_manager()
+            .get_logical_monitors()
+            .filter(lm => {
+                return lm.get_monitors()
+                    .some(m => m.get_backlight() && m.is_active());
+            });
+
+        if (monitors.length === 0) {
+            this._globalScale = null;
+        } else if (!this._globalScale) {
+            this._globalScale = new BrightnessScale(_('Brightness'), 1.0);
+            this._globalScale.connect('notify::value', () => {
+                if (this._inhibitUpdates)
+                    return;
+                this._globalScaleChanged = true;
+                this._sync();
+            });
+        }
+
+        this._monitorScales.clear();
+
+        for (const monitor of monitors) {
+            const scale = new MonitorBrightnessScale(monitor, 1.0);
+            scale._scaleChanged = true;
+
+            scale.connectObject(
+                'backlights-changed', () => this._sync(),
+                'notify::value',  () => {
+                    if (this._inhibitUpdates)
+                        return;
+                    scale._scaleChanged = true;
+                    this._sync();
+                }, this);
+
+            this._monitorScales.set(monitor, scale);
+        }
+
+        if (this._globalScale)
+            this._sync({showOSD: false});
+
+        this.emit('changed');
+    }
+
+    _sync({showOSD = true} = {}) {
+        if (this._inhibitUpdates)
+            return;
+        this._inhibitUpdates = true;
+
+        // Handle changed backlights
+        for (const scale of this._monitorScales.values())
+            scale.syncWithBacklight();
+
+        // Find scales which have been changed (and reset _scaleChanged)
+        const changedScales = [...this._monitorScales.values()].filter(s => {
+            const c = s._scaleChanged;
+            s._scaleChanged = false;
+            return c;
+        });
+
+        if (changedScales.length > 0) {
+            // update the factors of all the scales when a scale changes
+
+            // normalize everything to the maximum of all scales
+            const max = Math.max(...[...this._monitorScales.values()]
+                .map(s => s.value));
+
+            // if max is 0 we can't deduce any ratios, so don't try
+            if (max > 0.01) {
+                for (const scale of this._monitorScales.values())
+                    scale.updateScaleFactor(max);
+            }
+
+            // the global scale always follows the maximum, because one monitor
+            // scale is at the maximum and we want the global scale to be a
+            // factor we apply on the ratio of the monitor scales.
+            this._globalScale.value = max;
+
+            if (showOSD)
+                this._showOSD(changedScales);
+        } else if (this._globalScaleChanged) {
+            // if the global scale changed, update the monitor scales according
+            // to their scaleFactor and the global scale.
+
+            this._globalScaleChanged = false;
+
+            for (const scale of this._monitorScales.values())
+                scale.syncWithScale(this._globalScale);
+
+            if (showOSD)
+                this._showOSD(this._monitorScales.values());
+        }
+
+        // Update the actual backlight according to the new monitor brightnesses
+        for (const scale of this._monitorScales.values())
+            scale.setBacklight(scale.value);
+
+        this._inhibitUpdates = false;
+    }
+
+    _showOSD(monitorScales) {
+        const osdMonitors = {};
+        for (const scale of monitorScales) {
+            const level = scale.value;
+            osdMonitors[scale.monitor.get_number()] = {level};
+        }
+
+        Main.osdWindowManager.show(
+            Gio.Icon.new_for_string('display-brightness-symbolic'),
+            null,
+            osdMonitors
+        );
+    }
+});
+
+export const BrightnessScale = GObject.registerClass({
+    Properties: {
+        'value': GObject.ParamSpec.float(
+            'value', null, null,
+            GObject.ParamFlags.READWRITE,
+            0, 1.0, 1.0),
+    },
+}, class BrightnessScale extends GObject.Object {
+    constructor(name, value) {
+        super({
+            value,
+        });
+
+        this._name = name;
+    }
+
+    get name() {
+        return this._name;
+    }
+
+    get value() {
+        return this._value;
+    }
+
+    set value(value) {
+        this._setValue(value);
+    }
+
+    _setValue(value) {
+        value = Math.clamp(value, 0.0, 1.0);
+        if (Math.abs(value - this._value) < SCALE_VALUE_CHANGE_EPSILON)
+            return;
+        this._value = value;
+        this.notify('value');
+    }
+});
+
+const MonitorBrightnessScale = GObject.registerClass({
+    Signals: {
+        'backlights-changed': {},
+    },
+}, class MonitorBrightnessScale extends BrightnessScale {
+    constructor(monitor, value) {
+        const name = monitor.get_monitors()[0].get_display_name();
+
+        super(name, value);
+
+        this._monitor = monitor;
+        this._currentBacklightBrightness = -1;
+        this._scaleFactor = 1.0;
+
+        for (const backlight of this._getBacklights()) {
+            backlight.connectObject('notify::brightness', () => {
+                this.emit('backlights-changed');
+            }, this);
+        }
+    }
+
+    get monitor() {
+        return this._monitor;
+    }
+
+    _getBacklights() {
+        return this._monitor.get_monitors()
+            .filter(m => m.get_backlight() && m.is_active())
+            .map(m => m.get_backlight());
+    }
+
+    _getRelativeBrightness(backlight) {
+        const {brightness, brightnessMin: min, brightnessMax: max} = backlight;
+        return (brightness - min) / (max - min);
+    }
+
+    _setRelativeBrightness(backlight, brightness) {
+        const {brightnessMin: min, brightnessMax: max} = backlight;
+        backlight.brightness = min + ((max - min) * brightness);
+    }
+
+    syncWithBacklight() {
+        const [backlight] = this._getBacklights();
+
+        if (backlight.brightness === this._currentBacklightBrightness)
+            return false;
+        this._currentBacklightBrightness = backlight.brightness;
+
+        this.value = this._getRelativeBrightness(backlight);
+        return true;
+    }
+
+    syncWithScale(globalScale) {
+        this.value = globalScale.value * this._scaleFactor;
+    }
+
+    setBacklight(brightness) {
+        const backlights = this._getBacklights();
+        for (const backlight of backlights)
+            this._setRelativeBrightness(backlight, brightness);
+
+        this._currentBacklightBrightness = backlights[0].brightness;
+    }
+
+    updateScaleFactor(max) {
+        this._scaleFactor = this.value / max;
+    }
+});
index 76b917f48c17df8c3cebbffc33becb81c145e313..cd2417a6e0c650a9616b78debbbcc619a2babc9a 100644 (file)
@@ -9,6 +9,7 @@ import St from 'gi://St';
 import * as AccessDialog from './accessDialog.js';
 import * as AudioDeviceSelection from './audioDeviceSelection.js';
 import * as BreakManager from '../misc/breakManager.js';
+import * as BrightnessManager from '../misc/brightnessManager.js';
 import * as Config from '../misc/config.js';
 import * as Components from './components.js';
 import * as CtrlAltTab from './ctrlAltTab.js';
@@ -96,6 +97,7 @@ export let screenTimeDBus = null;
 export let breakManagerDispatcher = null;
 export let timeLimitsManager = null;
 export let timeLimitsDispatcher = null;
+export let brightnessManager = null;
 
 let _startDate;
 let _defaultCssStylesheet = null;
@@ -258,6 +260,8 @@ async function _initializeUI() {
     breakManagerDispatcher = new BreakManager.BreakDispatcher(breakManager);
     timeLimitsDispatcher = new TimeLimitsManager.TimeLimitsDispatcher(timeLimitsManager);
 
+    brightnessManager = new BrightnessManager.BrightnessManager();
+
     global.connect('shutdown', () => {
         // Block shutdown until the session history file has been written
         const loop = new GLib.MainLoop(null, false);
index b3edf7624ccef91311dbfc98e197c6e7417ad921..ee0829c96e3a3c2fea4e79150b183c2f4b7c686b 100644 (file)
@@ -14,6 +14,7 @@ js/gdm/authPrompt.js
 js/gdm/loginDialog.js
 js/gdm/util.js
 js/misc/breakManager.js
+js/misc/brightnessManager.js
 js/misc/systemActions.js
 js/misc/timeLimitsManager.js
 js/misc/util.js